package com.tungse.study.mini_spring;

import com.tungse.study.mini_spring.annotation.MyComponent;
import com.tungse.study.mini_spring.interceptor.MyHandlerInterceptorAdapter;
import com.tungse.study.mini_spring.web.DispatcherServlet;

import javax.validation.constraints.NotNull;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author Tungse
 */
public class ApplicationContext {

    private Map<String, Object> ioc = new HashMap<>();
    private Map<String, Object> iocCatch = new HashMap<>();
    private Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>();
    private List<MyBeanPostProcessor> postProcessors = new ArrayList<>();

    public ApplicationContext(@NotNull String packageName) throws Exception {
        initContext(packageName);

        DispatcherServlet bean = this.getBean(DispatcherServlet.class);
        List<MyHandlerInterceptorAdapter> beanInterceptors = this.getBeans(MyHandlerInterceptorAdapter.class);
        beanInterceptors.forEach(bean::addInterceptors);
    }

    private void initContext(@NotNull String packageName) throws Exception {
        scanPackage(packageName).stream().filter(this::sacnCreate).forEach(this::wrapper);
        initBeanPostProcessor();
        beanDefinitionMap.values().forEach(this::createBean);
    }

    private void initBeanPostProcessor() {
        beanDefinitionMap.values().stream()
                .filter(bd -> MyBeanPostProcessor.class.isAssignableFrom(bd.getBeanType()))
                .map(this::createBean)
                .map(bean -> (MyBeanPostProcessor) bean)
                .forEach(postProcessors::add);
    }

    protected boolean sacnCreate(@NotNull Class<?> type) {
        return type.isAnnotationPresent(MyComponent.class);
    }

    //创建bean
    protected Object createBean(@NotNull BeanDefinition beanDefinition) {
        String name = beanDefinition.getName();
        if (ioc.containsKey(name)) {
            return ioc.get(name);
        }
        if (iocCatch.containsKey(name)) {
            return iocCatch.get(name);
        }
        return doCreateBean(beanDefinition);
    }

    private Object doCreateBean(@NotNull BeanDefinition beanDefinition) {
        Constructor<?> constructor = beanDefinition.getConstructor();
        Object bean = null;
        try {
            //创建bean
            bean = constructor.newInstance();
            iocCatch.put(beanDefinition.getName(), bean);
            //自动注入属性
            autowiredBean(bean, beanDefinition);

            //初始化bean
            bean = initializeBean(bean, beanDefinition);

            iocCatch.remove(beanDefinition.getName());
            //放入容器
            ioc.put(beanDefinition.getName(), bean);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return bean;
    }

    private Object  initializeBean(Object bean, BeanDefinition beanDefinition) throws Exception {
        for(MyBeanPostProcessor postProcessor : postProcessors) {
            bean = postProcessor.postProcessBeforeInitialization(bean, beanDefinition.getName());
        }

        //执行初始化方法
        Method postConstructMethod = beanDefinition.getPostConstructMethod();
        if (postConstructMethod != null) {
            postConstructMethod.invoke(bean);
        }

        for(MyBeanPostProcessor postProcessor : postProcessors) {
            bean = postProcessor.postProcessAfterInitialization(bean, beanDefinition.getName());
        }
        return bean;
    }

    private void autowiredBean(Object bean, BeanDefinition beanDefinition) throws Exception {
        for (Field field : beanDefinition.getAutowiredFields()) {
            field.setAccessible(true);
            field.set(bean, this.getBean(field.getType()));
        }
    }

    //包装bean
    public BeanDefinition wrapper(Class<?> type) {
        BeanDefinition beanDefinition = new BeanDefinition(type);
        if (beanDefinitionMap.containsKey(beanDefinition.getName())) {
            throw new RuntimeException("bean name is duplicate");
        }
        beanDefinitionMap.put(beanDefinition.getName(), beanDefinition);

        return beanDefinition;
    }

    //扫描包
    public List<Class<?>> scanPackage(String packageName) throws Exception {
        List<Class<?>> list = new ArrayList<>();
        //a.b.c   a/b/c
        URL resource = this.getClass().getClassLoader().getResource(packageName.replace(".", File.separator));
        Path path = Path.of(resource.toURI());
        Files.walkFileTree(path, new SimpleFileVisitor<>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Path absolutePath = file.toAbsolutePath();
                if (absolutePath.toString().endsWith(".class")) {
                    String className = absolutePath.toString().replace(File.separator, ".").replace(".class", "");
                    className = className.substring(className.indexOf(packageName));
//                    System.out.println(className);
                    try {
                        list.add(Class.forName(className));
                    } catch (ClassNotFoundException e) {
                        throw new RuntimeException(e);
                    }
                }
                return FileVisitResult.CONTINUE;
            }
        });
        return list;
    }


    public <T> T getBean(String beanName) {
        if (beanName == null) {
            return null;
        }
        Object bean = null;
        if (iocCatch.containsKey(beanName)) {
            bean = iocCatch.get(beanName);
        } else {
            bean = this.ioc.get(beanName);
        }
        if (beanDefinitionMap.get(beanName) != null) {
            bean = createBean(beanDefinitionMap.get(beanName));
        }
        return (T)bean;
    }

    public <T> T getBean(Class<T> beanType) {
        BeanDefinition beanDefinition =
                beanDefinitionMap.values().stream().filter(bd -> beanType.isAssignableFrom(bd.getBeanType())).findAny().orElse(null);
        return getBean(beanDefinition.getName());
    }

    public <T> List<T> getBeans(Class<T> beanType) {

        return beanDefinitionMap.values().stream().filter(bd -> beanType.isAssignableFrom(bd.getBeanType()))
                .map(BeanDefinition::getName)
                .map(this::getBean)
                .map(bean -> (T) bean)
                .collect(Collectors.toList());
    }

    public Map<String, BeanDefinition> getBeanDefinitionMap() {
        return beanDefinitionMap;
    }
}
